Prevedere le Dimissioni dei Dipendenti: Una Strategia per le Risorse Umane basata sui Dati per Salifort Motors¶
Descrizione e Risultati Attesi¶
Questo progetto "Capstone" esplora i fattori che influenzano le dimissioni dei dipendenti presso Salifort Motors, con l’obiettivo di supportare il reparto HR 'Risorse Umane' nel miglioramento della retenzione e della soddisfazione generale dei dipendenti. Analizzando un dataset contenente punteggi di soddisfazione, cronologia delle valutazioni, assegnazioni dipartimentali e altro ancora, ho costruito modelli predittivi per identificare i dipendenti più propensi a lasciare l’azienda.
Progettato per simulare un reale flusso di lavoro di data science, questo progetto include:
Un Jupyter Notebook completamente documentato che descrive il processo di pulizia dei dati, l’analisi esplorativa (EDA), la modellazione e la valutazione.
Un Sintesi Esecutiva di una pagina per gli stakeholder, che evidenzia i risultati principali e raccomandazioni operative.
Ho utilizzato modelli di machine learning, nello specifico, Decision-Tree (alberi decisionali) e Random Forest (foreste casuali) per bilanciare accuratezza e interpretabilità. Le prestazioni dei modelli sono state valutate usando accuracy, precision, recall, F1-score e ROC-AUC.
Oltre agli aspetti tecnici, il progetto affronta considerazioni etiche nella modellazione delle dimmisione dei dipendenti accompagnata da visualizzazioni che facilitano la comprensione delle tendenze osservate. Inoltre, sono documentate le fasi di troubleshooting, le risorse consultate e le giustificazioni metodologiche lungo tutto il notebook.
📌 Fase 1: Fase di Pianificazione (Pace)¶
- Comprendere il problema aziendale
- Definire l'obiettivo: prevedere le dimmisioni dei dipendenti per supportare le decisioni HR
- Identificare la variabile target: left
📦 Pacchetti utilizzati¶
In questo progetto, ho utilizzato vari pacchetti Python per la gestione dei dati, la visualizzazione, la modellazione e la valutazione:
- pandas e numpy per la manipolazione dei dati e operazioni numeriche
- matplotlib e seaborn per la visualizzazione dei dati
- scikit-learn (sklearn) per costruire modelli di machine learning (regressione logistica, albero decisionale e foresta casuale), ottimizzare gli iperparametri e valutare le prestazioni
- pickle per salvare i modelli addestrati per usi futuri
# Data Manipulation
import pandas as pd
import numpy as np
pd.set_option('display.max_columns', None) # Show all columns when displaying a DataFrame
# Data Visualization
import matplotlib.pyplot as plt
import seaborn as sns
# Modeling - Algorithms
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.ensemble import RandomForestClassifier
# Modeling - Utilities
from sklearn.model_selection import train_test_split, GridSearchCV
# Evaluation Metrics
from sklearn.metrics import accuracy_score, precision_score, recall_score, \
f1_score, confusion_matrix, ConfusionMatrixDisplay, classification_report
from sklearn.metrics import roc_auc_score
import warnings
warnings.filterwarnings("ignore", message="invalid value encountered in cast")
# Model Saving
import pickle
🧭 Passaggio 1: Caricamento dei Dati ed Esplorazione Iniziale¶
df0 = pd.read_csv('HR_capstone_dataset.csv')
df0.head()
| satisfaction_level | last_evaluation | number_project | average_montly_hours | time_spend_company | Work_accident | left | promotion_last_5years | Department | salary | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.38 | 0.53 | 2 | 157 | 3 | 0 | 1 | 0 | sales | low |
| 1 | 0.80 | 0.86 | 5 | 262 | 6 | 0 | 1 | 0 | sales | medium |
| 2 | 0.11 | 0.88 | 7 | 272 | 4 | 0 | 1 | 0 | sales | medium |
| 3 | 0.72 | 0.87 | 5 | 223 | 5 | 0 | 1 | 0 | sales | low |
| 4 | 0.37 | 0.52 | 2 | 159 | 3 | 0 | 1 | 0 | sales | low |
df0.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 14999 entries, 0 to 14998 Data columns (total 10 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 satisfaction_level 14999 non-null float64 1 last_evaluation 14999 non-null float64 2 number_project 14999 non-null int64 3 average_montly_hours 14999 non-null int64 4 time_spend_company 14999 non-null int64 5 Work_accident 14999 non-null int64 6 left 14999 non-null int64 7 promotion_last_5years 14999 non-null int64 8 Department 14999 non-null object 9 salary 14999 non-null object dtypes: float64(2), int64(6), object(2) memory usage: 1.1+ MB
df0.describe()
| satisfaction_level | last_evaluation | number_project | average_montly_hours | time_spend_company | Work_accident | left | promotion_last_5years | |
|---|---|---|---|---|---|---|---|---|
| count | 14999.000000 | 14999.000000 | 14999.000000 | 14999.000000 | 14999.000000 | 14999.000000 | 14999.000000 | 14999.000000 |
| mean | 0.612834 | 0.716102 | 3.803054 | 201.050337 | 3.498233 | 0.144610 | 0.238083 | 0.021268 |
| std | 0.248631 | 0.171169 | 1.232592 | 49.943099 | 1.460136 | 0.351719 | 0.425924 | 0.144281 |
| min | 0.090000 | 0.360000 | 2.000000 | 96.000000 | 2.000000 | 0.000000 | 0.000000 | 0.000000 |
| 25% | 0.440000 | 0.560000 | 3.000000 | 156.000000 | 3.000000 | 0.000000 | 0.000000 | 0.000000 |
| 50% | 0.640000 | 0.720000 | 4.000000 | 200.000000 | 3.000000 | 0.000000 | 0.000000 | 0.000000 |
| 75% | 0.820000 | 0.870000 | 5.000000 | 245.000000 | 4.000000 | 0.000000 | 0.000000 | 0.000000 |
| max | 1.000000 | 1.000000 | 7.000000 | 310.000000 | 10.000000 | 1.000000 | 1.000000 | 1.000000 |
📊 Fase 2: Analisi Esplorativa dei Dati (EDA)¶
🔍 2.1: Panoramica delle Colonne¶
- Visualizzare i nomi delle colonne per verificare eventuali errori di battitura o incoerenze
- Identificare eventuali colonne che potrebbero richiedere una rinomina
print("Original column names:")
print(df0.columns.tolist())
Original column names: ['satisfaction_level', 'last_evaluation', 'number_project', 'average_montly_hours', 'time_spend_company', 'Work_accident', 'left', 'promotion_last_5years', 'Department', 'salary']
df0 = df0.rename(columns={
'average_montly_hours': 'average_monthly_hours',
'time_spend_company': 'tenure',
'Work_accident': 'work_accident',
'Department': 'department'
})
# Check for any misspelled column names (e.g., 'montly' instead of 'monthly')
for col in df0.columns:
if "montly" in col:
print(f"Possible typo found: {col}")
print("Renamed columns:")
print(df0.columns.tolist())
Renamed columns: ['satisfaction_level', 'last_evaluation', 'number_project', 'average_monthly_hours', 'tenure', 'work_accident', 'left', 'promotion_last_5years', 'department', 'salary']
💡 Osservazione¶
I nomi delle colonne sono stati puliti e standardizzati per garantire coerenza e chiarezza. Errori di battitura come average_montly_hours sono stati corretti, e le variabili in camel case (es. Work_accident, Department) sono state convertite in minuscolo. Queste modifiche aiutano a evitare errori e rendono il dataset più facile da utilizzare durante l’analisi.
2.2: Verifica dei Valori Mancanti¶
- Identificare se ci sono valori mancanti nel dataset
- Confermare la completezza dei dati prima di procedere
if df0.isna().sum().sum() > 0:
print("Ci sono valori mancanti nel dataset:")
print(df0.isna().sum()[df0.isna().sum() > 0])
else:
print("✅ Non ci sono valori mancanti nel dataset.")
✅ Non ci sono valori mancanti nel dataset.
💡 Osservazione¶
Non sono stati trovati valori mancanti nel dataset. Questo ci permette di procedere con l’analisi e la modellazione senza la necessità di imputazione o eliminazione di righe.
♻️ 2.3: Rilevamento e Gestione dei Duplicati¶
- Contare e ispezionare le righe duplicate
- Eliminarle se sono corrispondenze esatte e non utili per l’analisi
# Count duplicates
num_duplicates = df0.duplicated().sum()
# Calculate percentage
percent_duplicates = (num_duplicates / len(df0)) * 100
# Print results
print(f"⚠️ Ci sono {num_duplicates} righe duplicate nel dataset.")
print(f"Questo rappresenta il {percent_duplicates:.2f}% del totale dei dati.")
df0[df0.duplicated()].head()
⚠️ Ci sono 3008 righe duplicate nel dataset. Questo rappresenta il 20.05% del totale dei dati.
| satisfaction_level | last_evaluation | number_project | average_monthly_hours | tenure | work_accident | left | promotion_last_5years | department | salary | |
|---|---|---|---|---|---|---|---|---|---|---|
| 396 | 0.46 | 0.57 | 2 | 139 | 3 | 0 | 1 | 0 | sales | low |
| 866 | 0.41 | 0.46 | 2 | 128 | 3 | 0 | 1 | 0 | accounting | low |
| 1317 | 0.37 | 0.51 | 2 | 127 | 3 | 0 | 1 | 0 | sales | medium |
| 1368 | 0.41 | 0.52 | 2 | 132 | 3 | 0 | 1 | 0 | RandD | low |
| 1461 | 0.42 | 0.53 | 2 | 142 | 3 | 0 | 1 | 0 | sales | low |
# Inspecting some rows that contains duplicates
df0[df0.duplicated()].head(n = 10)
| satisfaction_level | last_evaluation | number_project | average_monthly_hours | tenure | work_accident | left | promotion_last_5years | department | salary | |
|---|---|---|---|---|---|---|---|---|---|---|
| 396 | 0.46 | 0.57 | 2 | 139 | 3 | 0 | 1 | 0 | sales | low |
| 866 | 0.41 | 0.46 | 2 | 128 | 3 | 0 | 1 | 0 | accounting | low |
| 1317 | 0.37 | 0.51 | 2 | 127 | 3 | 0 | 1 | 0 | sales | medium |
| 1368 | 0.41 | 0.52 | 2 | 132 | 3 | 0 | 1 | 0 | RandD | low |
| 1461 | 0.42 | 0.53 | 2 | 142 | 3 | 0 | 1 | 0 | sales | low |
| 1516 | 0.40 | 0.50 | 2 | 127 | 3 | 0 | 1 | 0 | IT | low |
| 1616 | 0.37 | 0.46 | 2 | 156 | 3 | 0 | 1 | 0 | sales | low |
| 1696 | 0.39 | 0.56 | 2 | 160 | 3 | 0 | 1 | 0 | sales | low |
| 1833 | 0.10 | 0.85 | 6 | 266 | 4 | 0 | 1 | 0 | sales | low |
| 12000 | 0.38 | 0.53 | 2 | 157 | 3 | 0 | 1 | 0 | sales | low |
# Removing duplicates.
df1 = df0.drop_duplicates(keep = 'first')
df1.head()
| satisfaction_level | last_evaluation | number_project | average_monthly_hours | tenure | work_accident | left | promotion_last_5years | department | salary | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.38 | 0.53 | 2 | 157 | 3 | 0 | 1 | 0 | sales | low |
| 1 | 0.80 | 0.86 | 5 | 262 | 6 | 0 | 1 | 0 | sales | medium |
| 2 | 0.11 | 0.88 | 7 | 272 | 4 | 0 | 1 | 0 | sales | medium |
| 3 | 0.72 | 0.87 | 5 | 223 | 5 | 0 | 1 | 0 | sales | low |
| 4 | 0.37 | 0.52 | 2 | 159 | 3 | 0 | 1 | 0 | sales | low |
💡 Osservazione¶
Su un totale di 14.999 record, sono stati identificati e rimossi 3.008 duplicati esatti (20,05%). Questo passaggio ha garantito un dataset più pulito composto da 11.991 record unici di dipendenti, evitando potenziali bias durante la fase di training del modello.
📈 2.4: Panoramica sulla Distribuzione delle Variabili¶
Prima di verificare la presenza di outlier, è importante comprendere come sono distribuite le variabili numeriche nel dataset. Questo aiuta a identificare eventuali asimmetrie, intervalli di valori comuni e potenziali anomalie.
# Plotting histograms for all numeric features
df1.hist(figsize=(14, 10), bins=30, edgecolor='black')
plt.suptitle('Distribution of Numeric Features', fontsize=16)
plt.tight_layout()
plt.show()
💡 Osservazione¶
La maggior parte delle variabili numeriche è di tipo categorico (es. number_project, tenure) o binario (es. work_accident, promotion_last_5years). Alcune variabili come satisfaction_level e average_monthly_hours mostrano maggiore variabilità, con raggruppamenti evidenti e possibili pattern multimodali. Questo controllo sulla distribuzione aiuta a stabilire un punto di partenza prima della rilevazione formale degli outlier e della modellazione.
🚩 2.5: Rilevamento degli Outlier¶
plt.figure(figsize = (6, 5))
plt.title('Boxplot per rilevare gli outlier nella variabile tenure', fontsize = 12)
plt.xticks(fontsize = 12)
plt.yticks(fontsize = 12)
sns.boxplot(x = df1['tenure'])
plt.show()
💡 Osservazione¶
Il boxplot sopra mostra diversi outlier (valori anomali) nella colonna tenure. Sebbene la maggior parte dei dipendenti resti in azienda tra i 3 e i 5 anni, esistono alcuni casi in cui la permanenza si estende tra i 6 e i 10 anni. Questi valori, considerati anomali secondo l'intervallo interquartile (IQR), potrebbero rappresentare dipendenti con elevata anzianità o situazioni particolari di .retention che meritano un'ulteriore analisi.
🔄 2.6: Relazioni tra Variabili e la Variabile Target (left)¶
Determinare il numero di righe che contengono outlier
# Computing the 25th percentile value in 'tenure'
percentile25 = df1['tenure'].quantile(0.25)
# Computing the 75th percentile value in 'tenure'
percentile75 = df1['tenure'].quantile(0.75)
# Computing the interquartile range in 'tenure'
iqr = percentile75 - percentile25
# Defining the upper limit and lower limit for non-outliers values in 'tenure'
upper_limit = percentile75 + 1.5 * iqr
lower_limit = percentile25 - 1.5 * iqr
print('Upper limit:', upper_limit)
print('Lower limit:', lower_limit)
# Identifying subset of the data, that contains outliers in 'tenure'
outliers = df1[(df1['tenure'] > upper_limit) | (df1['tenure'] < lower_limit)]
# Count how many rows in the data contain outliers in `tenure`
print("Number of rows in the data containing outliers in `tenure`:", len(outliers))
Upper limit: 5.5 Lower limit: 1.5 Number of rows in the data containing outliers in `tenure`: 824
💡 Osservazione¶
Secondo il metodo dell’intervallo interquartile (IQR), qualsiasi dipendente con una permanenza inferiore a 1,5 anni o superiore a 5,5 anni è considerato un outlier (valore anomalo). Sono stati identificati 824 outlier nella colonna tenure, indicando un numero significativo di dipendenti che hanno lasciato molto presto o che sono rimasti molto più a lungo rispetto all’intervallo tipico di 2–5 anni.
pAce: Fase di Analisi¶
- Eseguire l’EDA per analizzare le relazioni tra le variabili.
🔍 2.7: Riepilogo sulle Dimissioni dei Dipendenti¶
Quanti dipendenti hanno lasciato l’azienda e quale percentuale rappresentano rispetto al totale?
# Getting the number of people who left vs stayed.
counts = df1['left'].value_counts()
percentages = df1['left'].value_counts(normalize=True) * 100
# Getting the percentage of people who left vs stayed.
print(f'✅ Total employees: {df1.shape[0]}')
print(f'🟢 Stayed: {counts[0]} ({percentages[0]:.2f}%)')
print(f'🔴 Left: {counts[1]} ({percentages[1]:.2f}%)')
✅ Total employees: 11991 🟢 Stayed: 10000 (83.40%) 🔴 Left: 1991 (16.60%)
💡 Osservazione¶
Tra gli 11.991 dipendenti presenti nel dataset pulito, 1.991 dipendenti (16,60%) hanno lasciato l’azienda, mentre 10.000 (83,40%) sono rimasti. Questo squilibrio tra le classi è importante da tenere in considerazione durante la modellazione, poiché può influenzare le prestazioni degli algoritmi di classificazione.
📊 2.8: Ore Mensili vs. Numero di Progetti (Boxplot + Istogramma)¶
Grafici per visualizzare le relazioni tra le variabili nel dataset.
fig, ax = plt.subplots(1, 2, figsize = (22,8))
sns.boxplot(data = df1, x = 'average_monthly_hours', y = 'number_project', hue = 'left', orient = 'h', ax = ax [0])
ax[0].invert_yaxis()
ax[0].set_title('Monthly hours by number of projects', fontsize = '14')
tenure_stay = df1[df1['left']==0]['number_project']
tenure_left = df1[df1['left']==1]['number_project']
sns.histplot(data = df1, x = 'number_project', hue = 'left', multiple = 'dodge', shrink = 2, ax = ax[1])
ax[1].set_title('Number of projects histogram', fontsize = '14')
plt.show()
💡 Osservazione¶
Il boxplot mostra che i dipendenti con un carico di progetti molto basso (2) o molto alto (6–7) hanno maggiori probabilità di lasciare l’azienda. I dipendenti con un carico di lavoro moderato (3–5 progetti) tendono a restare, suggerendo che sia la sotto-utilizzazione che il sovraccarico possano contribuire a lasciare l'azienda.
Inoltre, i dipendenti che si sono licenziati oltre ad avere molti progetti svolgevano mediamente anche un numero significativamente più alto di ore mensili, indicando un potenziale rischio di burnout.
🔎 Verifica del licenziamento tra i dipendenti con carico di lavoro elevato (7 progetti)¶
Vedremo quanti dipendenti con esattamente 7 progetti sono rimasti in azienda e quanti l’hanno lasciata.
df1[df1['number_project']==7]['left'].value_counts()
left 1 145 Name: count, dtype: int64
💡 Osservazione¶
Tutti i dipendenti con 7 progetti hanno lasciato l’azienda, per un totale di 145 casi.
Questo suggerisce che un carico eccessivo di progetti possa essere insostenibile e rappresentiun chiaro segnale di possibili burnout o disimpegno da parte dei dipendenti.
🔍 Esaminare le Ore Mensili Medie rispetto ai Livelli di Soddisfazione¶
plt.figure(figsize=(14, 7))
sns.scatterplot(data=df1, x='average_monthly_hours', y='satisfaction_level', hue='left', alpha=0.4)
plt.axvline(x=166.67, color='#ff6361', label='166.67 hrs./mo.', ls='--')
plt.legend(labels=['166.67 hrs./mo.', 'left', 'stayed'])
plt.title('Monthly hours by last evaluation score', fontsize='14');
💡 Osservazione¶
Ore Mensili Medie vs. Livello di Soddisfazione
Lo scatterplot che confronta le ore mensili medie con il livello di soddisfazione, I colori distinguono tra dipendenti che hanno lasciato l’azienda e quelli che vi lavorano ancora., rivela due pattern distinti di abbandono:
Dipendenti Sovraccarichi e Insoddisfatti
- Un fitto gruppo di dipendenti con bassa soddisfazione e ore mensili molto elevate (oltre 250) ha scelto di lasciare l’azienda.
- Questo suggerisce un caso classico di burnout: lavorare a lungo senza essere soddisfatti porta a un’elevata probabilità di abbandono.
Dipendenti Performanti che se Ne Vanno
- Sorprendentemente, molti dipendenti con punteggi di soddisfazione elevati e molte ore lavorative hanno comunque lasciato l’azienda.
- Questo potrebbe indicare individui altamente performanti che si sono sentiti sovraccarichi o poco valorizzati, portandoli a dimissioni volontarie.
Una linea verticale di riferimento a 166,67 ore/mese (il carico di lavoro stimato per un tempo pieno) aiuta a distinguere i pattern di lavoro tipici da quelli eccessivi:
- La maggior parte dei dipendenti rimasti ha lavorato tra 150 e 250 ore/mese, riportando livelli di soddisfazione da moderati ad alti.
- I dipendenti che hanno lasciato si concentrano sia sotto che sopra questa fascia evidenziando più cause di abbandono: burnout, sotto-utilizzo o aspettative disattese.
Conclusione: sia il sovraccarico di lavoro che la mancanza di riconoscimento sono potenziali fattori che contribuiscono al turnover presso Salifort Motors.
📉 Visualizzare i Livelli di Soddisfazione in base all’Anzianità (Tenure)¶
fig, ax = plt.subplots(1, 2, figsize = (22, 8))
# Boxplot showing distributions of `satisfaction_level` by tenure, comparing employees who stayed versus those who left
sns.boxplot(data = df1, x = 'satisfaction_level', y = 'tenure', hue = 'left', orient = 'h', ax = ax[0])
ax[0].invert_yaxis()
ax[0].set_title('Tenure Histogram', fontsize = '14')
# Histogram for showing distribution of `tenure`, comparing employees who stayed versus those who left
tenure_stay = df1[df1['left'] == 0] ['tenure']
tenure_left = df1[df1['left'] == 1] ['tenure']
sns.histplot(data = df1, x = 'tenure', hue = 'left', multiple = 'dodge', shrink = 5, ax = ax[1])
ax[1].set_title('Tenure Histogram', fontsize = '14')
plt.show
<function matplotlib.pyplot.show(close=None, block=None)>
💡 Osservazione¶
Livello di Soddisfazione in base all’Anzianità (Tenure)
📊 Boxplot (Soddisfazione vs. Tenure)¶
- I dipendenti con bassa soddisfazione e un’anzianità compresa tra 2 e 4 anni mostrano una maggiore probabilità di lasciare l’azienda.
- È presente un cluster evidente di dipendenti che i sono licenziati, concentrato attorno ai 4 anni di anzianità, suggerendo che questo sia un punto critico di abbandono.
- I dipendenti con anzianità maggiore (8–10 anni) tendono a restare, mostrando livelli di soddisfazione relativamente stabili.
📈 Istogramma (Distribuzione dell’Anzianità per Stato Occupazionale)¶
- La maggior parte dei dipendenti ha un’anzianità compresa tra 3 e 5 anni, con un picco di licenziamenti in questo intervallo.
- Pochissimi dipendenti restano oltre i 7 anni e, tra questi, quasi tutti sono rimasti, suggerendo maggiore lealtà o seniority.
💡 Conclusione:¶
Il punto dei 4 anni di anzianità sembra essere una soglia critica per la soddisfazione e l’abbandono dei dipendenti. L’ufficio HR potrebbe considerare di introdurre iniziative di retention, promozioni o programmi di riconoscimento per i dipendenti che si avvicinano al quarto anno.
🧮 Confronto tra i Livelli Medi e Mediani di Soddisfazione: Chi è Rimasto vs. Chi ha Lasciato¶
Nel prossimo passaggio dell’analisi, calcolerò la media e la mediana dei punteggi di soddisfazione per i dipendenti che hanno lasciato l’azienda e quelli che sono rimasti.
df1.groupby(['left'])['satisfaction_level'].agg(['mean', 'median'])
| mean | median | |
|---|---|---|
| left | ||
| 0 | 0.667365 | 0.69 |
| 1 | 0.440271 | 0.41 |
💡 Osservazione¶
Media e Mediana dei Livelli di Soddisfazione in base allo Stato Occupazionale
- I dipendenti che hanno lasciato l’azienda presentano un livello medio di soddisfazione più basso (0,44) rispetto a quelli che sono rimasti (0,67).
- Anche i valori mediani riflettono lo stesso pattern: 0,41 per chi ha lasciato contro 0,69 per chi è rimasto.
- È interessante notare che, per i dipendenti rimasti, la media è leggermente inferiore alla mediana, suggerendo una distribuzione asimmetrica a sinistra un piccolo gruppo di dipendenti meno soddisfatti potrebbe abbassare la media.
- Questo rafforza le intuizioni precedenti: una bassa soddisfazione è fortemente associata a dimmisioni dei dipendenti presso Salifort Motors.
📊 Analizzare i Livelli di Retribuzione in base all’Anzianità¶
fig, ax = plt.subplots(1, 2, figsize = (20, 6))
# Short-tenured employee
tenure_short = df1[df1['tenure'] < 7]
# Long-tenured employee
tenure_long = df1[df1['tenure'] > 6]
sns.histplot(data = tenure_short, x = 'tenure', hue = 'salary', discrete = 1,
hue_order = ['low', 'medium', 'high'], multiple = 'dodge', shrink = .5, ax = ax[0])
ax[0].set_title('Salary histogram by tenure: short-tenured people', fontsize = '14')
sns.histplot(data = tenure_long, x = 'tenure', hue = 'salary', discrete = 1,
hue_order = ['low', 'medium', 'high'], multiple = 'dodge', shrink = .4, ax = ax[1])
ax[1].set_title('Salary histogram by tenure: long-tenured people', fontsize = '14')
Text(0.5, 1.0, 'Salary histogram by tenure: long-tenured people')
💡 Osservazione¶
Distribuzione degli Stipendi in base all’Anzianità
- Tra i dipendenti con poca anzianità (tenure < 7 anni), la maggior parte percepisce uno stipendio basso o medio, con pochissimi nella fascia alta.
- Tra i dipendenti con anzianità elevata (tenure ≥ 7 anni), osserviamo un pattern simile, la maggior parte rientra comunque nelle fasce bassa e media.
- Non ci sono prove evidenti che i dipendenti con maggiore anzianità ricevano stipendi più alti.
- Questo suggerisce che la longevità presso Salifort Motors non si traduce necessariamente in una retribuzione più elevata, il che potrebbe influire sulla motivazione o sulla retention del personale con più esperienza.
📉 Esplorare la Relazione tra Ore Mensili e Valutazione Finale¶
Per analizzare se esiste una correlazione tra le lunghe ore di lavoro e punteggi di valutazione elevati, verrà creato uno scatterplot tra average_monthly_hours e last_evaluation.
plt.figure(figsize = (14, 7))
sns.scatterplot(data = df1, x = 'average_monthly_hours', y = 'last_evaluation', hue = 'left', alpha = 0.4)
plt.axvline(x = 166.67, color = '#ff6361', label = '166.67 hrs./mo.', ls = '--')
plt.legend(labels = ['166.67 hrs./mo.', 'left', 'stayed'])
plt.title('Monthly hours by last evaluation score', fontsize='14');
💡 Osservazione:¶
Ore Mensili Medie vs. Valutazione Finale
Lo scatterplot rivela due cluster evidenti tra i dipendenti che hanno lasciato l’azienda:
Top Performer Sovraccarichi: Una concentrazione significativa di ex-dipendenti con valutazioni elevate che lavoravano ben oltre le 167 ore mensili, suggerendo burnout o mancanza di riconoscimento, nonostante l’ottima performance.
Dipendenti Poco Coinvolti e Sottoutilizzati: Un altro gruppo di dipendenti che hanno lasciato lavorava poco sotto la soglia di 166,67 ore e riceveva valutazioni relativamente basse, il che potrebbe riflettere disimpegno o scarso adattamento al ruolo.
Osservazioni aggiuntive:
- Sembra esserci una correlazione positiva tra le ore lavorate e il punteggio di valutazione, anche se non perfettamente lineare.
- Pochissimi dipendenti si trovano nel quadrante in alto a sinistra (alta valutazione, poche ore lavorate), il che implica che lavorare meno ore raramente coincide con valutazioni elevate.
- La maggior parte dei dipendenti — indipendentemente dal fatto che siano rimasti o abbiano lasciato, tende a lavorare ben oltre le 166,67 ore mensili, suggerendo una cultura aziendale basata su lunghe ore di lavoro.
Questi pattern indicano due cause principali di dimmisione: burnout tra i top performer e scarso coinvolgimento tra i dipendenti con performance più basse.
🏆 Analisi: I Dipendenti che Lavorano Molte Ore sono Stati Promossi?¶
Esamino se i dipendenti che hanno lavorato per molte ore al mese sono stati effettivamente promossi negli ultimi 5 anni.
plt.figure(figsize = (12, 3))
sns.scatterplot(data = df1, x = 'average_monthly_hours', y = 'promotion_last_5years', hue = 'left', alpha = 0.4)
plt.axvline(x = 166.67, color = '#ff6361', ls = '--')
plt.legend(labels = ['166.67 hrs./mo.', 'left', 'stayed'])
plt.title('Ore mensili rispetto alle promozioni negli ultimi cinque anni', fontsize = 14)
Text(0.5, 1.0, 'Ore mensili rispetto alle promozioni negli ultimi cinque anni')
💡 Osservazione¶
Ore Mensili vs. Promozione negli Ultimi 5 Anni
Lo scatterplot sopra evidenzia pattern importanti riguardo la promozione e il carico di lavoro:
- Pochissimi dipendenti che sono stati promossi negli ultimi cinque anni hanno lasciato l’azienda.
- Tra coloro che hanno lavorato il maggior numero di ore mensili, solo una piccola parte è stata promossa, suggerendo una possibile disconnessione tra impegno e riconoscimento.
- Un numero significativo di dipendenti che hanno lasciato l’azienda è concentrato sull’estrema destra del grafico (cioè con alte ore medie mensili), indicando un possibile burnout o insoddisfazione dovuta alla mancanza di premi o riconoscimenti nonostante l’elevato impegno.
Questi pattern suggeriscono che i dipendenti ad alte prestazioni che lavorano costantemente molte ore potrebbero non sentirsi adeguatamente valorizzati, contribuendo così al turnover presso Salifort Motors.
🧩 Distribuzione dei Dipendenti che si sono licenziati per Reparto¶
Analizzo come sono distribuiti i dipendenti che hanno lasciato l’azienda nei diversi dipartimenti.
df1['department'].value_counts()
department sales 3239 technical 2244 support 1821 IT 976 RandD 694 product_mng 686 marketing 673 accounting 621 hr 601 management 436 Name: count, dtype: int64
plt.figure(figsize = (10, 5))
sns.histplot(data = df1, x = 'department', hue = 'left', discrete = 1,
hue_order = [0, 1], multiple = 'dodge', shrink = .5)
plt.xticks(rotation = 45)
plt.title('Counts of Stayed/Left by Department', fontsize = 12);
💡 Osservazione¶
Pattern di dimmisione tra i Reparti
- Il grafico mostra che, sebbene il numero assoluto di dipendenti varia da un reparto all’altro (con
sales,technicalesupporttra i più numerosi), la proporzione di dipendenti che sono lincenziati risulta relativamente costante tra i reparti. - Nessun reparto presenta un tasso di turnover eccezionalmente alto o basso rispetto alla propria dimensione.
- Questo suggerisce che il reparto da solo potrebbe non essere un fattore determinante nel turnover dei dipendenti presso Salifort Motors, e che altri elementi, come il carico di lavoro, la soddisfazione o le opportunità di crescita, potrebbero avere un ruolo più significativo.
🧪 Analisi delle Correlazioni tra le Variabili¶
Infine, esamino le correlazioni più forti tra le variabili del dataset per identificare relazioni significative utili alla modellazione.
# Filter only numeric columns for correlation
numeric_df = df0.select_dtypes(include=['float64', 'int64'])
# Create the heatmap with only numeric columns
plt.figure(figsize=(10, 5))
heatmap = sns.heatmap(numeric_df.corr(),
vmin=-1,
vmax=1,
annot=True,
cmap=sns.color_palette('vlag', as_cmap=True))
heatmap.set_title('Correlation Heatmap', fontdict={'fontsize': 12}, pad=12)
Text(0.5, 1.0, 'Correlation Heatmap')
🔍 Analisi delle Correlazioni¶
La heatmap sopra mostra le correlazioni tra le variabili numeriche nel dataset.
Osservazioni Chiave:
- Il livello di soddisfazione ha la correlazione negativa più forte con l’abbandono del lavoro (
left), pari a -0.39. Questo indica che una bassa soddisfazione è un forte predittore di turnover. - Valutazione delle performance, numero di progetti e ore mensili lavorate sono positivamente correlati tra loro. Ciò suggerisce che i dipendenti ad alte prestazioni tendono a lavorare di più e a prendere in carico più progetti, aumentando il rischio di burnout.
- La correlazione tra la promozione negli ultimi 5 anni e le altre variabili è generalmente debole. Questo potrebbe indicare che le promozioni sono rare o non strettamente legate al carico di lavoro o alle performance.
- In modo sorprendente, l'anzianità aziendale ha solo una modesta correlazione positiva con le dimmisioni. Questo conferma le precedenti analisi, secondo cui sia le durate molto brevi che quelle molto lunghe influenzano il turnover in modo diverso.
💡 Osservazione¶
- I dipendenti che lavorano molte ore, gestiscono numerosi progetti e ricevono poca riconoscenza (tramite promozioni o buone valutazioni) hanno una maggiore probabilità di lasciare l’azienda.
- Questo pattern può riflettere una cattiva gestione organizzativa, dove i lavoratori migliori sono sovraccarichi o sottovalutati.
- La heatmap conferma quanto emerso durante l’analisi esplorativa: il rischio di burnout è reale, e la soddisfazione dei dipendenti è cruciale.
- Inoltre, i dipendenti con anzianità superiore ai 6 anni tendono a restare, il che può indicare stabilità lavorativa tra lo staff più esperto o strategie di fidelizzazione selettiva.
🧠 Raccomandazione: Il reparto HR dovrebbe concentrarsi sull’aumento della soddisfazione e sull’equilibrio del carico di lavoro, in particolare per i dipendenti ad alte prestazioni con media anzianità, per ridurre il rischio di abbandono.
paCe: Fase di Costruzione (Construct Stage)¶
- Costruire modelli per prevedere le dimmisioni dei dipendenti
- Scegliere le tecniche di modellazione più appropriate
- Verificare le assunzioni del modello
- Valutare i risultati e confrontare le performance dei modelli
Assunzioni della Regressione Logistica¶
- ✅ L’output è binario
- ✅ Le osservazioni sono indipendenti
- 🔄 Nessuna multicollinearità severa
- ⚠️ Nessun outlier estremo (gestito nell’EDA)
- 🔄 Relazione lineare tra le variabili indipendenti e i log-odds
- ✅ Dataset sufficientemente grande (11.991 righe)
Approccio di Modellazione A: Regressione Logistica¶
Passaggi di Preprocessing:
department: categorica → codifica one-hotsalary: ordinale → numerica (low=0, medium=1, high=2)
Prossimo step: usare
LabelEncoderomap()per la colonnasalaryepd.get_dummies()perdepartment.
Passo 3. Costruzione del Modello, Passo 4. Risultati e Valutazione¶
- Adattare un modello che preveda la variabile target utilizzando due o più variabili indipendenti
- Verificare le assunzioni del modello
- Valutare le performance del modello
# Copy the dataframe
df_enc = df1.copy()
# Encode the `salary` column as an ordinal numeric category
df_enc['salary'] = (
df_enc['salary'].astype('category')
.cat.set_categories(['low', 'medium', 'high'])
.cat.codes
)
# Dummy encode the `department` column
df_enc = pd.get_dummies(df_enc, drop_first=False)
df_enc.head()
| satisfaction_level | last_evaluation | number_project | average_monthly_hours | tenure | work_accident | left | promotion_last_5years | salary | department_IT | department_RandD | department_accounting | department_hr | department_management | department_marketing | department_product_mng | department_sales | department_support | department_technical | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.38 | 0.53 | 2 | 157 | 3 | 0 | 1 | 0 | 0 | False | False | False | False | False | False | False | True | False | False |
| 1 | 0.80 | 0.86 | 5 | 262 | 6 | 0 | 1 | 0 | 1 | False | False | False | False | False | False | False | True | False | False |
| 2 | 0.11 | 0.88 | 7 | 272 | 4 | 0 | 1 | 0 | 1 | False | False | False | False | False | False | False | True | False | False |
| 3 | 0.72 | 0.87 | 5 | 223 | 5 | 0 | 1 | 0 | 0 | False | False | False | False | False | False | False | True | False | False |
| 4 | 0.37 | 0.52 | 2 | 159 | 3 | 0 | 1 | 0 | 0 | False | False | False | False | False | False | False | True | False | False |
🔍 Heatmap di Correlazione (Sottoinsieme di Variabili)¶
Questa heatmap mostra le correlazioni a coppie tra alcune variabili numeriche selezionate nel dataset.
È utile per identificare la multicollinearità e individuare relazioni interessanti prima della costruzione del modello.
plt.figure(figsize = (10, 4))
sns.heatmap(df_enc[['satisfaction_level', 'last_evaluation', 'number_project', 'average_monthly_hours', 'tenure']]
.corr(), annot = True, cmap = 'crest')
plt.title('Correlation Heatmap of Selected Features', fontsize=12, pad=12)
plt.show
<function matplotlib.pyplot.show(close=None, block=None)>
💡 Osservazione¶
- La maggior parte delle variabili in questo sottoinsieme presenta correlazioni deboli o moderate tra loro.
- Nessuna correlazione è sufficientemente forte (sopra ±0.8) da sollevare preoccupazioni di multicollinearità per la regressione.
- In particolare,
number_project,average_monthly_hourselast_evaluationmostrano correlazioni moderate, queste relazioni potrebbero influenzare il comportamento del modello.
📊 Confronto del Tasso di dimmisioni dei Dipendenti tra i Reparti¶
# In the legend, 0 (purple color) represents employees who did not leave, 1 (red color) represents employees who left
pd.crosstab(df1['department'], df1['left']).plot(kind = 'bar', color = 'mr')
plt.title('Counts of Employee who left versus Stayed across departments')
plt.ylabel('Employee Count')
plt.xlabel('Department')
plt.show
<function matplotlib.pyplot.show(close=None, block=None)>
💡 Osservazione¶
- Sebbene reparti come vendite, supporto e tecnico abbiano avuto il numero complessivo più alto di dipendenti, hanno anche registrato un tasso di dimmisioni significativo.
- Al contrario, reparti come direzione, contabilità e ricerca e sviluppo (RandD) hanno avuto meno casi di dimmisioni.
- Tuttavia, nessun reparto si distingue per un tasso di dimmisini proporzionalmente elevato, il che suggerisce che il turnover sia un problema diffuso a livello aziendale, e non limitato a team specifici.
Approccio di Modellazione A: Regressione Logistica¶
Questa sezione implementa il modello di regressione logistica per prevedere le dimmisioni dei dipendenti.
Il processo include la pulizia dei dati, la rimozione degli outlier, la selezione della variabile target e delle caratteristiche, l’addestramento del modello e la valutazione delle prestazioni.
✂️ Rimozione degli Outlier dalla Colonna tenure¶
Poiché la regressione logistica è sensibile agli outlier, rimuoviamo i valori estremi nella colonna tenure identificati in precedenza.
df_logistic_reg = df_enc [(df_enc['tenure'] >= lower_limit) & (df_enc['tenure'] <= upper_limit)]
df_logistic_reg.head()
| satisfaction_level | last_evaluation | number_project | average_monthly_hours | tenure | work_accident | left | promotion_last_5years | salary | department_IT | department_RandD | department_accounting | department_hr | department_management | department_marketing | department_product_mng | department_sales | department_support | department_technical | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.38 | 0.53 | 2 | 157 | 3 | 0 | 1 | 0 | 0 | False | False | False | False | False | False | False | True | False | False |
| 2 | 0.11 | 0.88 | 7 | 272 | 4 | 0 | 1 | 0 | 1 | False | False | False | False | False | False | False | True | False | False |
| 3 | 0.72 | 0.87 | 5 | 223 | 5 | 0 | 1 | 0 | 0 | False | False | False | False | False | False | False | True | False | False |
| 4 | 0.37 | 0.52 | 2 | 159 | 3 | 0 | 1 | 0 | 0 | False | False | False | False | False | False | False | True | False | False |
| 5 | 0.41 | 0.50 | 2 | 153 | 3 | 0 | 1 | 0 | 0 | False | False | False | False | False | False | False | True | False | False |
🎯 Definire Variabile Target e Caratteristiche (Features)¶
y: La variabile target — indica se il dipendente ha lasciato l’azienda.X: Tutte le altre variabili utilizzate per la previsione, esclusaleft.
y = df_logistic_reg['left']
y.head()
0 1 2 1 3 1 4 1 5 1 Name: left, dtype: int64
X = df_logistic_reg.drop('left', axis = 1)
X.head()
| satisfaction_level | last_evaluation | number_project | average_monthly_hours | tenure | work_accident | promotion_last_5years | salary | department_IT | department_RandD | department_accounting | department_hr | department_management | department_marketing | department_product_mng | department_sales | department_support | department_technical | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.38 | 0.53 | 2 | 157 | 3 | 0 | 0 | 0 | False | False | False | False | False | False | False | True | False | False |
| 2 | 0.11 | 0.88 | 7 | 272 | 4 | 0 | 0 | 1 | False | False | False | False | False | False | False | True | False | False |
| 3 | 0.72 | 0.87 | 5 | 223 | 5 | 0 | 0 | 0 | False | False | False | False | False | False | False | True | False | False |
| 4 | 0.37 | 0.52 | 2 | 159 | 3 | 0 | 0 | 0 | False | False | False | False | False | False | False | True | False | False |
| 5 | 0.41 | 0.50 | 2 | 153 | 3 | 0 | 0 | 0 | False | False | False | False | False | False | False | True | False | False |
🧪 Suddividere il Dataset in Training e Test Set¶
Suddividiamo il dataset in un training set e un test set utilizzando un rapporto 75/25 e applichiamo la stratificazione per preservare l’equilibrio delle classi in entrambi i set.
# Split the data into training set and testing set
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.25, stratify = y, random_state = 42)
🔨 Costruzione e Addestramento del Modello di Regressione Logistica¶
Creo ora un modello di regressione logistica e lo addestriamo utilizzando il training set.
# Construct a logistic regression model and fit it to the training dataset
log_clf = LogisticRegression(random_state = 42, max_iter = 1000).fit(X_train, y_train)
Test del Modello di Regressione Logistica:¶
Utilizzo il modello per fare previsioni sul test set.
y_pred = log_clf.predict(X_test)
Valutazione delle Predizioni del Modello: Matrice di Confusione¶
# COmuting values for confusion matrix
log_cm = confusion_matrix(y_test, y_pred, labels = log_clf.classes_)
# Creating the display of confusion matrix
log_disp = ConfusionMatrixDisplay(confusion_matrix = log_cm,
display_labels = log_clf.classes_)
# Now plot the confusion matrix
log_disp.plot(values_format = '')
plt.show()
Veri negativi: (2165) — Numero di persone che non hanno lasciato l’azienda e che il modello ha correttamente previsto come tali.
Falsi positivi: (156) — Numero di persone che non hanno lasciato l’azienda ma che il modello ha predetto erroneamente come dimissionari.
Falsi negativi: (348) — Numero di persone che hanno lasciato l’azienda ma il modello non è riuscito a prevedere la loro uscita.
Veri positivi: (123) — Numero di persone che hanno lasciato l’azienda e che il modello ha correttamente previsto.
📌 Nota: Un modello perfetto prevederebbe correttamente tutti i casi, con zero falsi positivi e zero falsi negativi.
🔍 Valutazione delle Prestazioni del Modello¶
Successivamente, genererò un classification report che include precisione, recall, F1-score e accuratezza per valutare quanto bene si comporta il modello di regressione logistica.
Prima di interpretare i risultati, è importante controllare la distribuzione delle classi nella variabile target (left), per capire se uno sbilanciamento potrebbe influenzare queste metriche.
df_logistic_reg['left'].value_counts(normalize = True)
left 0 0.831468 1 0.168532 Name: proportion, dtype: float64
C'è una suddivisione approssimativa dell’83%-17%.
Quindi i dati non sono perfettamente bilanciati, ma nemmeno troppo sbilanciati.
Se il disequilibrio fosse stato più marcato, sarebbe stato opportuno effettuare un bilanciamento delle classi (ad es. tramite sottocampionamento o sovracampionamento).
In questo caso, però, è possibile proseguire con la valutazione del modello senza modificare la distribuzione delle classi.
🧮 Valutazione del Modello con Classification Report¶
Calcolo precisione, recall, F1-score e accuratezza complessiva per valutare le prestazioni del modello sul set di test.
target_names = ['Predicted would not leave', 'Predicted would leave']
print(classification_report(y_test, y_pred, target_names = target_names))
precision recall f1-score support
Predicted would not leave 0.86 0.93 0.90 2321
Predicted would leave 0.44 0.26 0.33 471
accuracy 0.82 2792
macro avg 0.65 0.60 0.61 2792
weighted avg 0.79 0.82 0.80 2792
💡 Osservazione¶
Valutazione delle Prestazioni del Modello
- Il modello si comporta bene nel prevedere i dipendenti che non hanno lasciato l’azienda (precisione: 0.86, recall: 0.93).
- Tuttavia, mostra difficoltà nel prevedere quelli che hanno lasciato (precisione: 0.44, recall: 0.26), una situazione comune nei compiti di classificazione sbilanciati.
- Questo indica che il modello è più conservativo e tende a prevedere la permanenza piuttosto che l’abbandono, riducendo i falsi allarmi ma mancando molti veri casi di dimmisioni.
Approccio di Modellazione B: Modelli Basati su Alberi¶
Questo approccio include l’implementazione di Decision Tree e Random Forest.
🎯 Definire Variabile Target e Caratteristiche (Features)¶
y: La variabile target — indica se il dipendente ha lasciato l'azienda.X: Tutte le altre variabili utilizzate per la previsione, escluseleft.
y = df_enc['left']
y.head()
0 1 1 1 2 1 3 1 4 1 Name: left, dtype: int64
# Selecting the features
X = df_enc.drop('left', axis = 1)
X.head()
| satisfaction_level | last_evaluation | number_project | average_monthly_hours | tenure | work_accident | promotion_last_5years | salary | department_IT | department_RandD | department_accounting | department_hr | department_management | department_marketing | department_product_mng | department_sales | department_support | department_technical | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.38 | 0.53 | 2 | 157 | 3 | 0 | 0 | 0 | False | False | False | False | False | False | False | True | False | False |
| 1 | 0.80 | 0.86 | 5 | 262 | 6 | 0 | 0 | 1 | False | False | False | False | False | False | False | True | False | False |
| 2 | 0.11 | 0.88 | 7 | 272 | 4 | 0 | 0 | 1 | False | False | False | False | False | False | False | True | False | False |
| 3 | 0.72 | 0.87 | 5 | 223 | 5 | 0 | 0 | 0 | False | False | False | False | False | False | False | True | False | False |
| 4 | 0.37 | 0.52 | 2 | 159 | 3 | 0 | 0 | 0 | False | False | False | False | False | False | False | True | False | False |
📦 Suddividere i Dati in Set di Addestramento e di Test¶
Suddividiamo i dati in un set di addestramento e uno di test per costruire e valutare i modelli in modo affidabile.
# Split the data into training, validating, and testing sets.
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.25, stratify = y, random_state = 0)
print(f"X_train shape: {X_train.shape}")
print(f"X_test shape: {X_test.shape}")
print(f"y_train distribution:\n{y_train.value_counts(normalize=True)}")
X_train shape: (8993, 18) X_test shape: (2998, 18) y_train distribution: left 0 0.833982 1 0.166018 Name: proportion, dtype: float64
🌳 Decision Tree – Round 1¶
Costruisco un modello di decision-tree e imposta una ricerca a griglia con convalida incrociata per cercare esaustivamente (GridSearchCV) i parametri ottimali del modello.
tree = DecisionTreeClassifier(random_state=0)
cv_params = {
'max_depth': [4, 6, 8, None],
'min_samples_leaf': [2, 5, 1],
'min_samples_split': [2, 4, 6]
}
scoring = ['accuracy', 'precision', 'recall', 'f1', 'roc_auc']
tree1 = GridSearchCV(tree, cv_params, scoring=scoring, cv=4, refit='roc_auc')
🔧 Addestra il Modello ad Albero Decisionale sui Dati di Addestramento¶
%%time
tree1.fit(X_train, y_train)
CPU times: total: 4.39 s Wall time: 4.4 s
GridSearchCV(cv=4, estimator=DecisionTreeClassifier(random_state=0),
param_grid={'max_depth': [4, 6, 8, None],
'min_samples_leaf': [2, 5, 1],
'min_samples_split': [2, 4, 6]},
refit='roc_auc',
scoring=['accuracy', 'precision', 'recall', 'f1', 'roc_auc'])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
GridSearchCV(cv=4, estimator=DecisionTreeClassifier(random_state=0),
param_grid={'max_depth': [4, 6, 8, None],
'min_samples_leaf': [2, 5, 1],
'min_samples_split': [2, 4, 6]},
refit='roc_auc',
scoring=['accuracy', 'precision', 'recall', 'f1', 'roc_auc'])DecisionTreeClassifier(max_depth=4, min_samples_leaf=5, random_state=0)
DecisionTreeClassifier(max_depth=4, min_samples_leaf=5, random_state=0)
✅ Il modello ad albero decisionale è stato addestrato con successo utilizzando una ricerca a griglia con convalida incrociata a 4 fold.
I parametri ottimali selezionati sono stati:max_depth=4emin_samples_leaf=5.
Questi vincoli aiutano a prevenire l'overfitting e a migliorare la generalizzazione su dati non visti.
🌳 Migliori Parametri e AUC per l’Albero Decisionale (Round 1)¶
# checking the best parameters
tree1.best_params_
{'max_depth': 4, 'min_samples_leaf': 5, 'min_samples_split': 2}
tree1.best_score_
0.969819392792457
💡 Osservazione¶
Si tratta di un AUC molto elevato (~0.97), che indica che il modello ad albero decisionale è altamente efficace nel classificare i dipendenti sulla base dei dati di addestramento.
I parametri selezionati suggeriscono inoltre un albero che bilancia profondità e capacità di generalizzazione, contribuendo a evitare l’overfitting.
📊 Estrazione e Visualizzazione delle Metriche di Cross-Validation¶
def make_results(model_name: str, model_object, metric: str):
"""
Returns a summary table of cross-validated performance metrics for a trained model.
Parameters:
model_name (str): Custom label for the model (e.g., 'Decision Tree')
model_object (GridSearchCV): The trained model object from GridSearchCV
metric (str): The main evaluation metric to identify the best model ('auc', 'precision', etc.)
Returns:
table (DataFrame): A one-row DataFrame containing precision, recall, F1, accuracy, and AUC
"""
# Map the metric name to its corresponding column in cv_results_
metric_dict = {
'auc': 'mean_test_roc_auc',
'precision': 'mean_test_precision',
'recall': 'mean_test_recall',
'f1': 'mean_test_f1',
'accuracy': 'mean_test_accuracy'
}
# Extract all results into a DataFrame
cv_results = pd.DataFrame(model_object.cv_results_)
# Get the row with the best score for the selected metric
best_row = cv_results.iloc[cv_results[metric_dict[metric]].idxmax(), :]
# Collect performance metrics
table = pd.DataFrame({
'model': [model_name],
'precision': [best_row.mean_test_precision],
'recall': [best_row.mean_test_recall],
'F1': [best_row.mean_test_f1],
'accuracy': [best_row.mean_test_accuracy],
'auc': [best_row.mean_test_roc_auc]
})
return table
# Generate CV results table for the decision tree model
tree1_cv_results = make_results('Decision Tree CV', tree1, 'auc')
tree1_cv_results
| model | precision | recall | F1 | accuracy | auc | |
|---|---|---|---|---|---|---|
| 0 | Decision Tree CV | 0.914552 | 0.916949 | 0.915707 | 0.971978 | 0.969819 |
💡 Osservazione¶
Tutti questi punteggi ottenuti dal modello ad albero decisionale sono forti indicatori di buone prestazioni.
Il modello mostra un'elevata accuratezza e un ottimo AUC, confermando che si comporta bene su più fold di validazione.
Tuttavia, è importante ricordare che gli alberi decisionali possono essere soggetti a overfitting.
Per affrontare questo problema e migliorare la capacità di generalizzazione, il prossimo passo è costruire un modello di random forest, che aggrega le previsioni di più alberi per ridurre la varianza e migliorare le prestazioni complessive.
🌲 Random Forest – Round 1¶
- Ora costruisco un modello di Random Forest e applico la grid search con convalida incrociata per identificare il miglior insieme di iperparametri.
Nota: Le Random Forest sono modelli di tipo ensemble che riducono l’overfitting combinando le previsioni di più alberi decisionali.
🛠️ Configurazione del Modello e Griglia di Parametri¶
# Instantiate model
rf = RandomForestClassifier(random_state=0)
# Define hyperparameter grid
cv_params = {
'max_depth': [3, 5, None],
'max_features': ['sqrt', None],
'max_samples': [0.7, 1.0],
'min_samples_leaf': [1, 2, 3],
'min_samples_split': [2, 3, 4],
'n_estimators': [300, 500]
}
# Define scoring metrics
scoring = ['accuracy', 'precision', 'f1', 'recall', 'roc_auc']
🚀 Addestramento del Modello Random Forest sui Dati di Training¶
%%time
# Grid search with cross-validation (4-fold)
rf1 = GridSearchCV(rf, cv_params, scoring=scoring, cv=4, refit='roc_auc')
rf1.fit(X_train, y_train)
C:\Users\wodoo\anaconda3\Lib\site-packages\numpy\ma\core.py:2820: RuntimeWarning: invalid value encountered in cast _data = np.array(data, dtype=dtype, copy=copy,
CPU times: total: 44min 47s Wall time: 45min 37s
GridSearchCV(cv=4, estimator=RandomForestClassifier(random_state=0),
param_grid={'max_depth': [3, 5, None],
'max_features': ['sqrt', None],
'max_samples': [0.7, 1.0],
'min_samples_leaf': [1, 2, 3],
'min_samples_split': [2, 3, 4],
'n_estimators': [300, 500]},
refit='roc_auc',
scoring=['accuracy', 'precision', 'f1', 'recall', 'roc_auc'])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
GridSearchCV(cv=4, estimator=RandomForestClassifier(random_state=0),
param_grid={'max_depth': [3, 5, None],
'max_features': ['sqrt', None],
'max_samples': [0.7, 1.0],
'min_samples_leaf': [1, 2, 3],
'min_samples_split': [2, 3, 4],
'n_estimators': [300, 500]},
refit='roc_auc',
scoring=['accuracy', 'precision', 'f1', 'recall', 'roc_auc'])RandomForestClassifier(max_depth=5, max_features=None, max_samples=0.7,
min_samples_split=4, n_estimators=500, random_state=0)RandomForestClassifier(max_depth=5, max_features=None, max_samples=0.7,
min_samples_split=4, n_estimators=500, random_state=0)💡 Osservazione¶
Il modello Random Forest ha completato una ricerca esaustiva dei parametri (grid search) e ha selezionato iperparametri che bilanciano profondità degli alberi, dimensione del campione e numero di stimatori.
Con max_depth=5, max_samples=0.7 e n_estimators=500, il modello è ottimizzato per ridurre l’overfitting e migliorare la generalizzazione.
Questa configurazione dovrebbe offrire prestazioni più robuste rispetto a un singolo albero decisionale, soprattutto sui dati non ancora visti.
💾 Specifica il Percorso per Salvare il Modello Pickle¶
Definisci il percorso locale dove verrà salvato il modello addestrato in formato .pickle.
# Define the directory path where the model will be saved
path = "C:\\Users\\wodoo\\Desktop\\model"
📦 Salva il Modello come File Pickle¶
Definisco due funzioni: una per salvare il modello in formato .pickle e una per ricaricarlo successivamente.
write_pickle(): salvare un modello addestrato in un file.pickleread_pickle(): caricare un modello salvato da un file.pickle
def write_pickle(path, model_object, save_as: str):
"""
Save a trained model as a .pickle file in the specified path.
Parameters:
path (str): Folder location where the model will be saved.
model_object: Trained model object to be pickled.
save_as (str): Filename (without extension) to save the model as.
"""
with open(path + save_as + '.pickle', 'wb') as to_write:
pickle.dump(model_object, to_write)
def read_pickle(path, saved_model_name: str):
"""
Load a pickled model from the specified path.
Parameters:
path (str): Folder location where the model is stored.
saved_model_name (str): Filename (without extension) of the saved model.
Returns:
model: The loaded model object.
"""
with open(path + saved_model_name + '.pickle', 'rb') as to_read:
model = pickle.load(to_read)
return model
📦 Salvataggio e Caricamento del Modello Random Forest con Pickle¶
Utilizzo le funzioni definite per salvare il modello Random Forest come file .pickle
e poi ricaricarlo in memoria per un uso futuro.
# Write pickle
write_pickle(path, rf1, 'hr_rf1')
# Read pickle
rf1 = read_pickle(path, 'hr_rf1')
🌲 Migliori Parametri e AUC per Random Forest (Round 1)¶
Il modello Random Forest ha selezionato i seguenti iperparametri ottimali dopo la ricerca con convalida incrociata:
max_depth = 5max_samples = 0.7n_estimators = 500min_samples_split = 4max_features = None
L’AUC ottenuto sul set di addestramento è stato di 0.980, un valore molto alto che indica una forte capacità del modello nel distinguere tra dipendenti che lasciano e quelli che restano.
# Check best AUC score on CV
rf1.best_score_
0.9804250949807172
# Check best params
rf1.best_params_
{'max_depth': 5,
'max_features': None,
'max_samples': 0.7,
'min_samples_leaf': 1,
'min_samples_split': 4,
'n_estimators': 500}
💡 Osservazione¶
Il modello Random Forest ha raggiunto un AUC molto elevato (~0.98) durante la convalida incrociata, superando il modello Decision Tree del Round 1.
Gli iperparametri selezionati indicano un ensemble profondo di 500 alberi con campionamento al 70% e potatura minima, permettendo al modello di catturare pattern complessi nei dati pur mantenendo una buona capacità di generalizzazione.
📊 Confronto delle Metriche di Valutazione: Decision Tree vs Random Forest (Round 1)¶
Confrontiamo ora i risultati della convalida incrociata dei due modelli per determinare quale offre le prestazioni migliori sui dati di addestramento.
# Get all CV scores
rf1_cv_results = make_results('random forest cv', rf1, 'auc')
print(tree1_cv_results)
print(rf1_cv_results)
model precision recall F1 accuracy auc
0 Decision Tree CV 0.914552 0.916949 0.915707 0.971978 0.969819
model precision recall F1 accuracy auc
0 random forest cv 0.950023 0.915614 0.932467 0.977983 0.980425
💡 Osservazione¶
Il modello di Random Forest supera il Decision Tree in tutte le metriche di valutazione, ad eccezione del recall, dove la differenza è minima (~0.001).
Questo suggerisce che la Random Forest fornisce prestazioni predittive complessive migliori, mantenendo una sensibilità quasi identica.
Come previsto, l'approccio con ensemble offre maggiore robustezza e accuratezza rispetto a un singolo albero decisionale.
💡 Osservazione¶
Il modello di Random Forest supera il Decision Tree in tutte le metriche di valutazione, ad eccezione del recall, dove la differenza è minima (~0.001).
Questo suggerisce che la Random Forest fornisce prestazioni predittive complessive migliori, mantenendo una sensibilità quasi identica.
Come previsto, l'approccio con ensemble offre maggiore robustezza e accuratezza rispetto a un singolo albero decisionale.
def get_scores(model_name: str, model, X_test_data, y_test_data):
"""
Generate a table of performance metrics for a fitted model on the test set.
Parameters:
model_name (str): Name of the model for the output table.
model (GridSearchCV): Trained GridSearchCV object with .best_estimator_.
X_test_data (array): Feature data from the test set.
y_test_data (array): Target values from the test set.
Returns:
DataFrame: Precision, recall, F1, accuracy, and AUC scores.
"""
preds = model.best_estimator_.predict(X_test_data)
auc = roc_auc_score(y_test_data, preds)
accuracy = accuracy_score(y_test_data, preds)
precision = precision_score(y_test_data, preds)
recall = recall_score(y_test_data, preds)
f1 = f1_score(y_test_data, preds)
return pd.DataFrame({
'model': [model_name],
'precision': [precision],
'recall': [recall],
'f1': [f1],
'accuracy': [accuracy],
'AUC': [auc]
})
🧪 Valutazione Finale del Modello sul Test Set¶
Per valutare quanto bene il modello si generalizza su dati nuovi, analizzo le sue prestazioni sul test set non visto durante l’addestramento.
Definisco una funzione che calcola le principali metriche di performance basandosi sulle predizioni del modello.
# Get predictions on test data
rf1_test_scores = get_scores('random forest1 test', rf1, X_test, y_test)
rf1_test_scores
| model | precision | recall | f1 | accuracy | AUC | |
|---|---|---|---|---|---|---|
| 0 | random forest1 test | 0.964211 | 0.919679 | 0.941418 | 0.980987 | 0.956439 |
💡 Osservazione¶
Il modello ha ottenuto prestazioni solide sul test set, con risultati molto simili a quelli della validazione.
Questa coerenza suggerisce che il modello di Random Forest generalizza bene su dati non visti e non è soggetto a overfitting.
Poiché il test set è stato riservato esclusivamente alla valutazione finale, si puo essere fiduciosi che queste metriche riflettano le prestazioni nel mondo reale.
🔧 Feature Engineering¶
Sebbene i punteggi di valutazione del Round 1 siano stati molto alti, potrebbero essere eccessivamente ottimistici a causa di un possibile data leakage.
Il data leakage si verifica quando il modello ha accesso durante l’addestramento a informazioni che non sarebbero realisticamente disponibili in uno scenario di applicazione reale. Questo può portare a prestazioni artificialmente elevate che non si generalizzano bene su nuovi dati.
In questo caso, due variabili risultano critiche:
satisfaction_level: È improbabile che l’azienda disponga di dati affidabili sul livello di soddisfazione per tutti i dipendenti, specialmente prima della loro eventuale uscita.average_monthly_hours: Questo valore potrebbe riflettere un comportamento successivo, come la riduzione delle ore dopo che un dipendente ha già mostrato segnali di disimpegno o è stato individuato per un licenziamento.
✅ Strategia per il Round 2¶
- Eliminerò la variabile
satisfaction_levelper evitare di basare il modello su un’informazione potenzialmente non disponibile o “inquinata”. - Creare una nuova variabile binaria chiamata
overworked, che indicherà se un dipendente lavora significativamente più delle ore medie previste (es. oltre 200 ore/mese).
Questo consente di rappresentare la pressione del carico di lavoro senza usare direttamente il numero di ore, che potrebbe essere fuorviante.
Questi cambiamenti mirano a rendere il modello più realistico e applicabile in un vero contesto HR, dove segnali predittivi chiari e affidabili sono fondamentali.
Rimuovere satisfaction_level per Evitare Possibile Data Leakage¶
# Drop `satisfaction_level` and save the result in a new DataFrame
df2 = df_enc.drop('satisfaction_level', axis=1)
df2.head()
| last_evaluation | number_project | average_monthly_hours | tenure | work_accident | left | promotion_last_5years | salary | department_IT | department_RandD | department_accounting | department_hr | department_management | department_marketing | department_product_mng | department_sales | department_support | department_technical | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.53 | 2 | 157 | 3 | 0 | 1 | 0 | 0 | False | False | False | False | False | False | False | True | False | False |
| 1 | 0.86 | 5 | 262 | 6 | 0 | 1 | 0 | 1 | False | False | False | False | False | False | False | True | False | False |
| 2 | 0.88 | 7 | 272 | 4 | 0 | 1 | 0 | 1 | False | False | False | False | False | False | False | True | False | False |
| 3 | 0.87 | 5 | 223 | 5 | 0 | 1 | 0 | 0 | False | False | False | False | False | False | False | True | False | False |
| 4 | 0.52 | 2 | 159 | 3 | 0 | 1 | 0 | 0 | False | False | False | False | False | False | False | True | False | False |
🔍 Ispezionare il Carico di Lavoro: Preparazione alla Creazione della Variabile overworked¶
# Temporarily copy `average_monthly_hours` into new column `overworked`
df2['overworked'] = df2['average_monthly_hours']
# Check distribution range of working hours
print('Max hours:', df2['overworked'].max())
print('Min hours:', df2['overworked'].min())
Max hours: 310 Min hours: 96
💡 Osservazione¶
Un dipendente full-time che lavora 8 ore al giorno, 5 giorni a settimana, per 50 settimane all’anno, lavora in media circa 166,67 ore al mese.
Per individuare carichi di lavoro insolitamente elevati, definirò un dipendente come overworked se supera le 175 ore al mese.
Questa soglia mi permette di creare una nuova variabile binaria:
1= overworked (sovraccarico di lavoro)0= not overworked (carico normale)
La trasformazione sarà eseguita con:
df2['overworked'] = (df2['overworked'] > 175).astype(int)
🔧 Creare la Variabile Binaria overworked Basata sulle Ore Mensili¶
# Define `overworked` as working more than 175 hours/month
df2['overworked'] = (df2['overworked'] > 175).astype(int)
df2['overworked'].head()
0 0 1 1 2 1 3 1 4 0 Name: overworked, dtype: int32
🧹 Eliminare average_monthly_hours (Sostituita da overworked)¶
# Drop the now-redundant average_monthly_hours column
df2 = df2.drop('average_monthly_hours', axis=1)
df2.head()
| last_evaluation | number_project | tenure | work_accident | left | promotion_last_5years | salary | department_IT | department_RandD | department_accounting | department_hr | department_management | department_marketing | department_product_mng | department_sales | department_support | department_technical | overworked | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.53 | 2 | 3 | 0 | 1 | 0 | 0 | False | False | False | False | False | False | False | True | False | False | 0 |
| 1 | 0.86 | 5 | 6 | 0 | 1 | 0 | 1 | False | False | False | False | False | False | False | True | False | False | 1 |
| 2 | 0.88 | 7 | 4 | 0 | 1 | 0 | 1 | False | False | False | False | False | False | False | True | False | False | 1 |
| 3 | 0.87 | 5 | 5 | 0 | 1 | 0 | 0 | False | False | False | False | False | False | False | True | False | False | 1 |
| 4 | 0.52 | 2 | 3 | 0 | 1 | 0 | 0 | False | False | False | False | False | False | False | True | False | False | 0 |
🎯 Definire Target e Variabili (Dopo Feature Engineering)¶
y: La variabile target — indica se un dipendente ha lasciato l'azienda.X: Tutte le altre variabili predittive, escludendoleft, utilizzate per costruire i modelli.
# Isolate the target variable
y = df2['left']
# Select the remaining features
X = df2.drop('left', axis=1)
📦 Suddividi i Dati in Set di Addestramento e di Test¶
I dati vengono suddivisi in un training set (75%) e un test set (25%), utilizzando la stratificazione per mantenere la proporzione tra dipendenti che hanno lasciato e quelli che sono rimasti.
Questo garantisce che entrambi i set riflettano correttamente la distribuzione della variabile target left.
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.25, stratify = y, random_state = 0)
🌳 Decision Tree – Round 2 (con Feature Engineering)¶
In questo secondo round, ricostruiamo il modello ad albero decisionale dopo aver modificato il set di dati tramite feature engineering:
satisfaction_levelè stato rimosso per evitare il rischio di data leakage.- È stata creata una nuova variabile binaria
overworkedper rappresentare carichi di lavoro eccessivi.
Applico di nuovo la ricerca su griglia (GridSearchCV) per identificare i migliori iperparametri e valuteremo le prestazioni del nuovo modello.
# Instantiate a decision tree classifier
tree = DecisionTreeClassifier(random_state=0)
# Define hyperparameter grid
cv_params = {
'max_depth': [4, 6, 8, None],
'min_samples_leaf': [2, 5, 1],
'min_samples_split': [2, 4, 6]
}
# Define scoring metrics
scoring = ['accuracy', 'precision', 'recall', 'f1', 'roc_auc']
🔄 Fit del Modello Decision Tree (Round 2)¶
Addestro il nuovo modello ad albero decisionale sui dati modificati, utilizzando GridSearchCV con 4-fold cross-validation.
Questo ci permette di trovare la combinazione ottimale di iperparametri e ridurre il rischio di overfitting.
%%time
tree2 = GridSearchCV(tree, cv_params, scoring=scoring, cv=4, refit='roc_auc')
tree2.fit(X_train, y_train)
CPU times: total: 4.84 s Wall time: 4.89 s
GridSearchCV(cv=4, estimator=DecisionTreeClassifier(random_state=0),
param_grid={'max_depth': [4, 6, 8, None],
'min_samples_leaf': [2, 5, 1],
'min_samples_split': [2, 4, 6]},
refit='roc_auc',
scoring=['accuracy', 'precision', 'recall', 'f1', 'roc_auc'])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
GridSearchCV(cv=4, estimator=DecisionTreeClassifier(random_state=0),
param_grid={'max_depth': [4, 6, 8, None],
'min_samples_leaf': [2, 5, 1],
'min_samples_split': [2, 4, 6]},
refit='roc_auc',
scoring=['accuracy', 'precision', 'recall', 'f1', 'roc_auc'])DecisionTreeClassifier(max_depth=6, min_samples_leaf=2, min_samples_split=6,
random_state=0)DecisionTreeClassifier(max_depth=6, min_samples_leaf=2, min_samples_split=6,
random_state=0)🌳 Migliori Parametri e AUC Score per Decision Tree (Round 2)¶
Il modello ottimizzato ha selezionato i seguenti iperparametri:
max_depth = 6min_samples_leaf = 2min_samples_split = 6
Il punteggio AUC ottenuto durante la cross-validation è di ~0.96, indicando un'ottima capacità predittiva anche dopo la rimozione delle variabili sensibili.
Questi risultati confermano che il modello conserva una buona performance pur utilizzando un set di caratteristiche più realistico e privo di possibili leakage.
# Check best params
tree2.best_params_
{'max_depth': 6, 'min_samples_leaf': 2, 'min_samples_split': 6}
# Check best AUC score on CV
tree2.best_score_
0.9586752505340426
💡 Osservazione¶
Questo modello offre prestazioni eccellenti, anche dopo la rimozione di variabili come satisfaction_level e average_monthly_hours.
Il valore AUC elevato (~0.96) dimostra che il modello è ancora in grado di individuare pattern significativi utilizzando un set di caratteristiche ridotto ma più realistico.
📊 Confronto dei punteggi di valutazione: Decision Tree Round 1 vs Round 2¶
# Get all CV scores
tree2_cv_results = make_results('decision tree2 cv', tree2, 'auc')
print(tree1_cv_results)
print(tree2_cv_results)
model precision recall F1 accuracy auc
0 Decision Tree CV 0.914552 0.916949 0.915707 0.971978 0.969819
model precision recall F1 accuracy auc
0 decision tree2 cv 0.856693 0.903553 0.878882 0.958523 0.958675
💡 Osservazione¶
Alcune metriche di valutazione sono leggermente diminuite nel Round 2, come previsto, a causa della riduzione del numero di variabili.
Tuttavia, il modello mostra ancora prestazioni complessivamente solide, dimostrando che le caratteristiche ingegnerizzate hanno mantenuto un buon potere predittivo pur riducendo il rischio di data leakage.
🌲 Random Forest – Round 2 (con Feature Engineering)¶
# Instantiate the random forest classifier
rf = RandomForestClassifier(random_state=0) # you can keep using rf (no need for rf2 here)
# Define hyperparameter grid
cv_params = {
'max_depth': [3, 5, None],
'max_features': ['sqrt', None],
'max_samples': [0.7, 1.0],
'min_samples_leaf': [1, 2, 3],
'min_samples_split': [2, 3, 4],
'n_estimators': [300, 500]
}
# Define scoring metrics
scoring = ['accuracy', 'precision', 'f1', 'recall', 'roc_auc']
# Set up GridSearchCV
rf2 = GridSearchCV(rf, cv_params, scoring=scoring, cv=4, refit='roc_auc')
🔄 Addestramento del Modello Random Forest (Round 2)¶
%%time
rf2.fit(X_train, y_train)
CPU times: total: 34min 8s Wall time: 34min 42s
GridSearchCV(cv=4, estimator=RandomForestClassifier(random_state=0),
param_grid={'max_depth': [3, 5, None],
'max_features': ['sqrt', None],
'max_samples': [0.7, 1.0],
'min_samples_leaf': [1, 2, 3],
'min_samples_split': [2, 3, 4],
'n_estimators': [300, 500]},
refit='roc_auc',
scoring=['accuracy', 'precision', 'f1', 'recall', 'roc_auc'])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
GridSearchCV(cv=4, estimator=RandomForestClassifier(random_state=0),
param_grid={'max_depth': [3, 5, None],
'max_features': ['sqrt', None],
'max_samples': [0.7, 1.0],
'min_samples_leaf': [1, 2, 3],
'min_samples_split': [2, 3, 4],
'n_estimators': [300, 500]},
refit='roc_auc',
scoring=['accuracy', 'precision', 'f1', 'recall', 'roc_auc'])RandomForestClassifier(max_samples=0.7, min_samples_leaf=3, n_estimators=300,
random_state=0)RandomForestClassifier(max_samples=0.7, min_samples_leaf=3, n_estimators=300,
random_state=0)💾 Salvare e Ricaricare il Modello Random Forest Round 2 con Pickle¶
# Write pickle
write_pickle(path, rf2, 'hr_rf2')
# Read in pickle
rf2 = read_pickle(path, 'hr_rf2')
🌲 Migliori Parametri e Punteggio AUC per Random Forest (Round 2)¶
# Check best parameters
rf2.best_params_
{'max_depth': None,
'max_features': 'sqrt',
'max_samples': 0.7,
'min_samples_leaf': 3,
'min_samples_split': 2,
'n_estimators': 300}
# Check best AUC score from cross-validation
rf2.best_score_
0.96873476079196
📊 Confronto dei punteggi di valutazione: Albero Decisionale vs Random Forest (Round 2)¶
# Get evaluation results for Random Forest Round 2
rf2_cv_results = make_results('random forest2 cv', rf2, 'auc')
# Display comparison between decision tree and random forest (Round 2)
print(tree2_cv_results)
print(rf2_cv_results)
model precision recall F1 accuracy auc
0 decision tree2 cv 0.856693 0.903553 0.878882 0.958523 0.958675
model precision recall F1 accuracy auc
0 random forest2 cv 0.911854 0.866715 0.888685 0.963972 0.968735
💡 Osservazione¶
I punteggi sono leggermente diminuiti in questo round a causa della feature engineering, ma la random forest continua a superare l’albero decisionale in quasi tutte le metriche.
Se prendo l’AUC come indicatore principale, la random forest resta la scelta migliore, mantenendo una forte capacità di generalizzazione e allo stesso tempo riducendo il rischio di data leakage.
🧪 Valutazione finale sul Test Set (Random Forest Round 2)¶
# Get predictions on test data
rf2_test_scores = get_scores('random forest2 test', rf2, X_test, y_test)
rf2_test_scores
| model | precision | recall | f1 | accuracy | AUC | |
|---|---|---|---|---|---|---|
| 0 | random forest2 test | 0.904564 | 0.875502 | 0.889796 | 0.963976 | 0.928551 |
💡 Osservazione¶
Questo sembra essere un modello finale stabile e performante.
La sua performance sul test set rispecchia da vicino i risultati della cross-validation, suggerendo una buona capacità di generalizzazione sui dati non visti.
📉 Matrice di Confusione per Random Forest (Round 2)¶
# Generate array of values for confusion matrix
preds = rf2.best_estimator_.predict(X_test)
cm = confusion_matrix(y_test, preds, labels=rf2.classes_)
# Plot confusion matrix
disp = ConfusionMatrixDisplay(confusion_matrix=cm,
display_labels=rf2.classes_)
disp.plot(values_format='');
💡 Osservazione¶
Il modello prevede più falsi positivi (46) che falsi negativi (62), il che significa che alcuni dipendenti potrebbero essere erroneamente segnalati come propensi a lasciare quando non lo sono.
Nonostante ciò, il numero di veri positivi (436) e veri negativi (2454) indica che questo resta un modello solido con una buona affidabilità predittiva.
🌲 Suddivisioni dell'Albero Decisionale (Round 2)¶
# Plot the tree
plt.figure(figsize=(85,20))
plot_tree(tree2.best_estimator_, max_depth=6, fontsize=14, feature_names=X.columns,
class_names={0:'stayed', 1:'left'}, filled=True);
plt.show()
📝 Nota: Facendo doppio clic sull’output dell’albero in Jupyter si ingrandisce e si possono ispezionare le singole suddivisioni.
🌿 Importanza delle Feature (Albero Decisionale Round 2)¶
# Get feature importances
tree2_importances = pd.DataFrame(tree2.best_estimator_.feature_importances_,
columns = ['gini_importance'],
index = X.columns)
# Filter and sort
tree2_importances = tree2_importances.sort_values(by='gini_importance', ascending=False)
tree2_importances = tree2_importances[tree2_importances['gini_importance'] != 0]
# Visualize
tree2_importances.plot(kind='barh', figsize=(8,5), legend=False)
plt.title("Feature Importance (Decision Tree Round 2)")
plt.xlabel("Gini Importance")
plt.gca().invert_yaxis()
plt.show()
Barplot per visualizzare l'importanza delle feature nell'albero decisionale.
sns.barplot(data=tree2_importances, x="gini_importance", y=tree2_importances.index, orient='h')
plt.title("Decision Tree: Feature Importances for Employee Leaving", fontsize=12)
plt.ylabel("Feature")
plt.xlabel("Importance")
plt.show()
💡 Osservazione¶
Il grafico sopra mostra che in questo modello ad albero decisionale, last_evaluation, number_project, tenure e overworked hanno l'importanza più alta — in questo ordine.
Queste variabili hanno contribuito maggiormente alla previsione dell'abbandono da parte di un dipendente, secondo l'importanza di Gini.
🌲 Importanza delle Feature nella Random Forest (Round 2)¶
Ora, visualizziamo l'importanza delle feature per il modello random forest.
# Get feature importances
feat_impt = rf2.best_estimator_.feature_importances_
# Get indices of top 10 features
ind = np.argpartition(rf2.best_estimator_.feature_importances_, -10)[-10:]
# Get column labels of top 10 features
feat = X.columns[ind]
# Filter `feat_impt` to consist of top 10 feature importances
feat_impt = feat_impt[ind]
y_df = pd.DataFrame({'Feature':feat,'Importance':feat_impt})
y_sort_df = y_df.sort_values('Importance')
fig = plt.figure()
ax1 = fig.add_subplot(111)
y_sort_df.plot(kind = 'barh', ax=ax1, x = 'Feature', y = 'Importance')
ax1.set_title('Random Forest: Feature Importances for Employee Leaving', fontsize = 12)
ax1.set_ylabel('Feature')
ax1.set_xlabel('Importance')
plt.show()
💡 Osservazione¶
Il grafico sopra mostra che nel modello random forest, le variabili number_project, tenure, last_evaluation e overworked hanno l'importanza più alta, in quest’ordine.
Queste feature sono le più utili per prevedere l'abbandono dei dipendenti — e corrispondono strettamente a quelle identificate nel modello ad albero decisionale.
pacE: Fase di Esecuzione (Execution)¶
- Interpretare e comunicare la performance del modello
- Condividere con gli stakeholder raccomandazioni concrete e basate sui dati
✏️ Panoramica sulle Metriche di Valutazione¶
- AUC: Misura la capacità del modello di distinguere correttamente tra le classi.
- Precision: Proporzione delle previsioni positive effettivamente corrette.
- Recall: Proporzione dei positivi reali correttamente identificati.
- Accuracy: Proporzione complessiva di previsioni corrette.
- F1-score: Media armonica tra precision e recall — utile per classi sbilanciate.
Passaggio 4. Risultati Finali e Impatto sul Business¶
- Interpretare i risultati del modello nel contesto aziendale
- Utilizzare le metriche di performance per validare la qualità del modello
- Presentare ai decisori le principali conclusioni e raccomandazioni
📊 Sintesi dei Risultati dei Modelli¶
Regressione Logistica
Ha raggiunto un’accuracy dell’83% sul test set, con medie ponderate di:
- Precision: 80%
- Recall: 83%
- F1-score: 80%
Albero Decisionale (Round 2)
Dopo la feature engineering, il modello ad albero decisionale ha raggiunto:
- AUC: 93,8%
- Accuracy: 96,2%
- Precision: 87,0%
- Recall: 90,4%
- F1-score: 88,7%
Random Forest (Round 2)
Ha superato entrambi i modelli precedenti, ottenendo i punteggi più alti nella maggior parte delle metriche e mostrando una forte capacità di generalizzazione sul test set.
✅ Conclusioni, Raccomandazioni e Prossimi Passi¶
I modelli e l'importanza delle feature indicano che l'attrito dei dipendenti è fortemente influenzato dal sovraccarico di lavoro, numero di progetti, durata del rapporto lavorativo e punteggi di valutazione.
💼 Raccomandazioni Aziendali¶
- Stabilire un limite al numero di progetti simultanei per dipendente.
- Analizzare perché i dipendenti con una durata lavorativa di quattro anni mostrano un'elevata insoddisfazione.
- Offrire incentivi chiari o compensazioni per ore di lavoro aggiuntive.
- Migliorare la comunicazione riguardo al pagamento degli straordinari e alle aspettative di carico di lavoro.
- Promuovere discussioni a livello di team e aziendale per affrontare la cultura del lavoro e il burnout.
- Ripensare le modalità di valutazione delle performance, punteggi elevati non dovrebbero essere associati esclusivamente a carichi di lavoro estremi.
🚀 Prossimi Passi¶
- Indagare eventuali casi di data leakage, specialmente relativi alla feature
last_evaluation. - Valutare la rimozione di
last_evaluationosatisfaction_levelper osservare come si comportano i modelli senza queste variabili. - Esplorare obiettivi alternativi di modellazione, come prevedere la performance anziché l'abbandono.
- L’applicazione di un clustering con K-means potrebbe rivelare gruppi distinti di dipendenti, offrendo ulteriori spunti per piani di fidelizzazione.